Passed
Push — master ( 445067...6ce435 )
by Johan
02:18
created

action_container.js ➔ actionHash   F

Complexity

Conditions 36

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 0
c 0
b 0
f 0
cc 36

How to fix   Complexity   

Complexity

Complex classes like action_container.js ➔ actionHash often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/** internal
2
 * class ActionContainer
3
 *
4
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
5
 **/
6
7
'use strict';
8
9
var format = require('util').format;
10
11
// Constants
12
var c = require('./const');
13
14
var $$ = require('./utils');
15
16
//Actions
17
var ActionHelp = require('./action/help');
18
var ActionAppend = require('./action/append');
19
var ActionAppendConstant = require('./action/append/constant');
20
var ActionCount = require('./action/count');
21
var ActionStore = require('./action/store');
22
var ActionStoreConstant = require('./action/store/constant');
23
var ActionStoreTrue = require('./action/store/true');
24
var ActionStoreFalse = require('./action/store/false');
25
var ActionVersion = require('./action/version');
26
var ActionSubparsers = require('./action/subparsers');
27
28
// Errors
29
var argumentErrorHelper = require('./argument/error');
30
31
/**
32
 * new ActionContainer(options)
33
 *
34
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
35
 *
36
 * ##### Options:
37
 *
38
 * - `description` -- A description of what the program does
39
 * - `prefixChars`  -- Characters that prefix optional arguments
40
 * - `argumentDefault`  -- The default value for all arguments
41
 * - `conflictHandler` -- The conflict handler to use for duplicate arguments
42
 **/
43
var ActionContainer = module.exports = function ActionContainer(options) {
44
  options = options || {};
45
46
  this.description = options.description;
47
  this.argumentDefault = options.argumentDefault;
48
  this.prefixChars = options.prefixChars || '';
49
  this.conflictHandler = options.conflictHandler;
50
51
  // set up registries
52
  this._registries = {};
53
54
  // register actions
55
  this.register('action', null, ActionStore);
56
  this.register('action', 'store', ActionStore);
57
  this.register('action', 'storeConst', ActionStoreConstant);
58
  this.register('action', 'storeTrue', ActionStoreTrue);
59
  this.register('action', 'storeFalse', ActionStoreFalse);
60
  this.register('action', 'append', ActionAppend);
61
  this.register('action', 'appendConst', ActionAppendConstant);
62
  this.register('action', 'count', ActionCount);
63
  this.register('action', 'help', ActionHelp);
64
  this.register('action', 'version', ActionVersion);
65
  this.register('action', 'parsers', ActionSubparsers);
66
67
  // raise an exception if the conflict handler is invalid
68
  this._getHandler();
69
70
  // action storage
71
  this._actions = [];
72
  this._optionStringActions = {};
73
74
  // groups
75
  this._actionGroups = [];
76
  this._mutuallyExclusiveGroups = [];
77
78
  // defaults storage
79
  this._defaults = {};
80
81
  // determines whether an "option" looks like a negative number
82
  // -1, -1.5 -5e+4
83
  this._regexpNegativeNumber = new RegExp('^[-]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$');
84
85
  // whether or not there are any optionals that look like negative
86
  // numbers -- uses a list so it can be shared and edited
87
  this._hasNegativeNumberOptionals = [];
88
};
89
90
// Groups must be required, then ActionContainer already defined
91
var ArgumentGroup = require('./argument/group');
92
var MutuallyExclusiveGroup = require('./argument/exclusive');
93
94
//
95
// Registration methods
96
//
97
98
/**
99
 * ActionContainer#register(registryName, value, object) -> Void
100
 * - registryName (String) : object type action|type
101
 * - value (string) : keyword
102
 * - object (Object|Function) : handler
103
 *
104
 *  Register handlers
105
 **/
106
ActionContainer.prototype.register = function (registryName, value, object) {
107
  this._registries[registryName] = this._registries[registryName] || {};
108
  this._registries[registryName][value] = object;
109
};
110
111
ActionContainer.prototype._registryGet = function (registryName, value, defaultValue) {
112
  if (arguments.length < 3) {
113
    defaultValue = null;
114
  }
115
  return this._registries[registryName][value] || defaultValue;
116
};
117
118
//
119
// Namespace default accessor methods
120
//
121
122
/**
123
 * ActionContainer#setDefaults(options) -> Void
124
 * - options (object):hash of options see [[Action.new]]
125
 *
126
 * Set defaults
127
 **/
128
ActionContainer.prototype.setDefaults = function (options) {
129
  options = options || {};
130
  for (var property in options) {
131
    if ($$.has(options, property)) {
132
      this._defaults[property] = options[property];
133
    }
134
  }
135
136
  // if these defaults match any existing arguments, replace the previous
137
  // default on the object with the new one
138
  this._actions.forEach(function (action) {
139
    if ($$.has(options, action.dest)) {
140
      action.defaultValue = options[action.dest];
141
    }
142
  });
143
};
144
145
/**
146
 * ActionContainer#getDefault(dest) -> Mixed
147
 * - dest (string): action destination
148
 *
149
 * Return action default value
150
 **/
151
ActionContainer.prototype.getDefault = function (dest) {
152
  var result = $$.has(this._defaults, dest) ? this._defaults[dest] : null;
153
154
  this._actions.forEach(function (action) {
155
    if (action.dest === dest && $$.has(action, 'defaultValue')) {
156
      result = action.defaultValue;
157
    }
158
  });
159
160
  return result;
161
};
162
//
163
// Adding argument actions
164
//
165
166
/**
167
 * ActionContainer#addArgument(args, options) -> Object
168
 * - args (String|Array): argument key, or array of argument keys
169
 * - options (Object): action objects see [[Action.new]]
170
 *
171
 * #### Examples
172
 * - addArgument([ '-f', '--foo' ], { action: 'store', defaultValue: 1, ... })
173
 * - addArgument([ 'bar' ], { action: 'store', nargs: 1, ... })
174
 * - addArgument('--baz', { action: 'store', nargs: 1, ... })
175
 **/
176
ActionContainer.prototype.addArgument = function (args, options) {
177
  args = args;
178
  options = options || {};
179
180
  if (typeof args === 'string') {
181
    args = [ args ];
182
  }
183
  if (!Array.isArray(args)) {
184
    throw new TypeError('addArgument first argument should be a string or an array');
185
  }
186
  if (typeof options !== 'object' || Array.isArray(options)) {
187
    throw new TypeError('addArgument second argument should be a hash');
188
  }
189
190
  // if no positional args are supplied or only one is supplied and
191
  // it doesn't look like an option string, parse a positional argument
192
  if (!args || args.length === 1 && this.prefixChars.indexOf(args[0][0]) < 0) {
193
    if (args && !!options.dest) {
194
      throw new Error('dest supplied twice for positional argument');
195
    }
196
    options = this._getPositional(args, options);
197
198
    // otherwise, we're adding an optional argument
199
  } else {
200
    options = this._getOptional(args, options);
201
  }
202
203
  // if no default was supplied, use the parser-level default
204
  if (typeof options.defaultValue === 'undefined') {
205
    var dest = options.dest;
206
    if ($$.has(this._defaults, dest)) {
207
      options.defaultValue = this._defaults[dest];
208
    } else if (typeof this.argumentDefault !== 'undefined') {
209
      options.defaultValue = this.argumentDefault;
210
    }
211
  }
212
213
  // create the action object, and add it to the parser
214
  var ActionClass = this._popActionClass(options);
215
  if (typeof ActionClass !== 'function') {
216
    throw new Error(format('Unknown action "%s".', ActionClass));
217
  }
218
  var action = new ActionClass(options);
219
220
  // throw an error if the action type is not callable
221
  var typeFunction = this._registryGet('type', action.type, action.type);
222
  if (typeof typeFunction !== 'function') {
223
    throw new Error(format('"%s" is not callable', typeFunction));
224
  }
225
226
  return this._addAction(action);
227
};
228
229
/**
230
 * ActionContainer#addArgumentGroup(options) -> ArgumentGroup
231
 * - options (Object): hash of options see [[ArgumentGroup.new]]
232
 *
233
 * Create new arguments groups
234
 **/
235
ActionContainer.prototype.addArgumentGroup = function (options) {
236
  var group = new ArgumentGroup(this, options);
237
  this._actionGroups.push(group);
238
  return group;
239
};
240
241
/**
242
 * ActionContainer#addMutuallyExclusiveGroup(options) -> ArgumentGroup
243
 * - options (Object): {required: false}
244
 *
245
 * Create new mutual exclusive groups
246
 **/
247
ActionContainer.prototype.addMutuallyExclusiveGroup = function (options) {
248
  var group = new MutuallyExclusiveGroup(this, options);
249
  this._mutuallyExclusiveGroups.push(group);
250
  return group;
251
};
252
253
ActionContainer.prototype._addAction = function (action) {
254
  var self = this;
255
256
  // resolve any conflicts
257
  this._checkConflict(action);
258
259
  // add to actions list
260
  this._actions.push(action);
261
  action.container = this;
262
263
  // index the action by any option strings it has
264
  action.optionStrings.forEach(function (optionString) {
265
    self._optionStringActions[optionString] = action;
266
  });
267
268
  // set the flag if any option strings look like negative numbers
269
  action.optionStrings.forEach(function (optionString) {
270
    if (optionString.match(self._regexpNegativeNumber)) {
271
      if (!self._hasNegativeNumberOptionals.some(Boolean)) {
272
        self._hasNegativeNumberOptionals.push(true);
273
      }
274
    }
275
  });
276
277
  // return the created action
278
  return action;
279
};
280
281
ActionContainer.prototype._removeAction = function (action) {
282
  var actionIndex = this._actions.indexOf(action);
283
  if (actionIndex >= 0) {
284
    this._actions.splice(actionIndex, 1);
285
  }
286
};
287
288
ActionContainer.prototype._addContainerActions = function (container) {
289
  // collect groups by titles
290
  var titleGroupMap = {};
291
  this._actionGroups.forEach(function (group) {
292
    if (titleGroupMap[group.title]) {
293
      throw new Error(format('Cannot merge actions - two groups are named "%s".', group.title));
294
    }
295
    titleGroupMap[group.title] = group;
296
  });
297
298
  // map each action to its group
299
  var groupMap = {};
300
  function actionHash(action) {
301
    // unique (hopefully?) string suitable as dictionary key
302
    return action.getName();
303
  }
304
  container._actionGroups.forEach(function (group) {
305
    // if a group with the title exists, use that, otherwise
306
    // create a new group matching the container's group
307
    if (!titleGroupMap[group.title]) {
308
      titleGroupMap[group.title] = this.addArgumentGroup({
309
        title: group.title,
310
        description: group.description
311
      });
312
    }
313
314
    // map the actions to their new group
315
    group._groupActions.forEach(function (action) {
316
      groupMap[actionHash(action)] = titleGroupMap[group.title];
317
    });
318
  }, this);
319
320
  // add container's mutually exclusive groups
321
  // NOTE: if add_mutually_exclusive_group ever gains title= and
322
  // description= then this code will need to be expanded as above
323
  var mutexGroup;
324
  container._mutuallyExclusiveGroups.forEach(function (group) {
325
    mutexGroup = this.addMutuallyExclusiveGroup({
326
      required: group.required
327
    });
328
    // map the actions to their new mutex group
329
    group._groupActions.forEach(function (action) {
330
      groupMap[actionHash(action)] = mutexGroup;
331
    });
332
  }, this);  // forEach takes a 'this' argument
333
334
  // add all actions to this container or their group
335
  container._actions.forEach(function (action) {
336
    var key = actionHash(action);
337
    if (groupMap[key]) {
338
      groupMap[key]._addAction(action);
339
    } else {
340
      this._addAction(action);
341
    }
342
  });
343
};
344
345
ActionContainer.prototype._getPositional = function (dest, options) {
346
  if (Array.isArray(dest)) {
347
    dest = dest[0];
348
  }
349
  // make sure required is not specified
350
  if (options.required) {
351
    throw new Error('"required" is an invalid argument for positionals.');
352
  }
353
354
  // mark positional arguments as required if at least one is
355
  // always required
356
  if (options.nargs !== c.OPTIONAL && options.nargs !== c.ZERO_OR_MORE) {
357
    options.required = true;
358
  }
359
  if (options.nargs === c.ZERO_OR_MORE && typeof options.defaultValue === 'undefined') {
360
    options.required = true;
361
  }
362
363
  // return the keyword arguments with no option strings
364
  options.dest = dest;
365
  options.optionStrings = [];
366
  return options;
367
};
368
369
ActionContainer.prototype._getOptional = function (args, options) {
370
  var prefixChars = this.prefixChars;
371
  var optionStrings = [];
372
  var optionStringsLong = [];
373
374
  // determine short and long option strings
375
  args.forEach(function (optionString) {
376
    // error on strings that don't start with an appropriate prefix
377
    if (prefixChars.indexOf(optionString[0]) < 0) {
378
      throw new Error(format('Invalid option string "%s": must start with a "%s".',
379
        optionString,
380
        prefixChars
381
      ));
382
    }
383
384
    // strings starting with two prefix characters are long options
385
    optionStrings.push(optionString);
386
    if (optionString.length > 1 && prefixChars.indexOf(optionString[1]) >= 0) {
387
      optionStringsLong.push(optionString);
388
    }
389
  });
390
391
  // infer dest, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
392
  var dest = options.dest || null;
393
  delete options.dest;
394
395
  if (!dest) {
396
    var optionStringDest = optionStringsLong.length ? optionStringsLong[0] : optionStrings[0];
397
    dest = $$.trimChars(optionStringDest, this.prefixChars);
398
399
    if (dest.length === 0) {
400
      throw new Error(
401
        format('dest= is required for options like "%s"', optionStrings.join(', '))
402
      );
403
    }
404
    dest = dest.replace(/-/g, '_');
405
  }
406
407
  // return the updated keyword arguments
408
  options.dest = dest;
409
  options.optionStrings = optionStrings;
410
411
  return options;
412
};
413
414
ActionContainer.prototype._popActionClass = function (options, defaultValue) {
415
  defaultValue = defaultValue || null;
416
417
  var action = (options.action || defaultValue);
418
  delete options.action;
419
420
  var actionClass = this._registryGet('action', action, action);
421
  return actionClass;
422
};
423
424
ActionContainer.prototype._getHandler = function () {
425
  var handlerString = this.conflictHandler;
426
  var handlerFuncName = '_handleConflict' + $$.capitalize(handlerString);
427
  var func = this[handlerFuncName];
428
  if (typeof func === 'undefined') {
429
    var msg = 'invalid conflict resolution value: ' + handlerString;
430
    throw new Error(msg);
431
  } else {
432
    return func;
433
  }
434
};
435
436
ActionContainer.prototype._checkConflict = function (action) {
437
  var optionStringActions = this._optionStringActions;
438
  var conflictOptionals = [];
439
440
  // find all options that conflict with this option
441
  // collect pairs, the string, and an existing action that it conflicts with
442
  action.optionStrings.forEach(function (optionString) {
443
    var conflOptional = optionStringActions[optionString];
444
    if (typeof conflOptional !== 'undefined') {
445
      conflictOptionals.push([ optionString, conflOptional ]);
446
    }
447
  });
448
449
  if (conflictOptionals.length > 0) {
450
    var conflictHandler = this._getHandler();
451
    conflictHandler.call(this, action, conflictOptionals);
452
  }
453
};
454
455
ActionContainer.prototype._handleConflictError = function (action, conflOptionals) {
456
  var conflicts = conflOptionals.map(function (pair) { return pair[0]; });
457
  conflicts = conflicts.join(', ');
458
  throw argumentErrorHelper(
459
    action,
460
    format('Conflicting option string(s): %s', conflicts)
461
  );
462
};
463
464
ActionContainer.prototype._handleConflictResolve = function (action, conflOptionals) {
465
  // remove all conflicting options
466
  var self = this;
467
  conflOptionals.forEach(function (pair) {
468
    var optionString = pair[0];
469
    var conflictingAction = pair[1];
470
    // remove the conflicting option string
471
    var i = conflictingAction.optionStrings.indexOf(optionString);
472
    if (i >= 0) {
473
      conflictingAction.optionStrings.splice(i, 1);
474
    }
475
    delete self._optionStringActions[optionString];
476
    // if the option now has no option string, remove it from the
477
    // container holding it
478
    if (conflictingAction.optionStrings.length === 0) {
479
      conflictingAction.container._removeAction(conflictingAction);
480
    }
481
  });
482
};
483